﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using RestSharp.Extensions;

namespace RestSharp.Deserializers
{
   public class JsonDeserializer : IDeserializer
   {
      public JsonDeserializer()
      {
         Culture = CultureInfo.InvariantCulture;
      }

      public CultureInfo Culture { get; set; }

      #region IDeserializer Members

      public string RootElement { get; set; }
      public string Namespace { get; set; }
      public string DateFormat { get; set; }

      public T Deserialize<T>(IRestResponse response)
      {
         T target = Activator.CreateInstance<T>();

         if (target is IList)
         {
            Type objType = target.GetType();

            if (RootElement.HasValue())
            {
               object root = FindRoot(response.Content);
               target = (T) BuildList(objType, root);
            }
            else
            {
               object data = SimpleJson.DeserializeObject(response.Content);
               target = (T) BuildList(objType, data);
            }
         }
         else if (target is IDictionary)
         {
            object root = FindRoot(response.Content);
            target = (T) BuildDictionary(target.GetType(), root);
         }
         else
         {
            object root = FindRoot(response.Content);
            Map(target, (IDictionary<string, object>) root);
         }

         return target;
      }

      #endregion

      private object FindRoot(string content)
      {
         IDictionary<string, object> data = (IDictionary<string, object>) SimpleJson.DeserializeObject(content);
         if (RootElement.HasValue() && data.ContainsKey(RootElement))
            return data[RootElement];
         return data;
      }

      private void Map(object target, IDictionary<string, object> data)
      {
         Type objType = target.GetType();
         List<PropertyInfo> props = objType.GetProperties(BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance).Where(p => p.CanWrite).ToList();

         foreach (PropertyInfo prop in props)
         {
            Type type = prop.PropertyType;

            JsonNameAttribute jsonNameAttribute = prop.GetAttribute<JsonNameAttribute>();

            string name = jsonNameAttribute != null ? jsonNameAttribute.Name : prop.Name;
            string actualName = name.GetNameVariants(Culture).FirstOrDefault(data.ContainsKey);
            object value = (actualName != null ? data[actualName] : null);

            if (value == null) continue;

            string stringValue = Convert.ToString(value, Culture);

            // check for nullable and extract underlying type
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof (Nullable<>))
               type = type.GetGenericArguments()[0];

            if (type.IsPrimitive)
            {
               // no primitives can contain quotes so we can safely remove them
               // allows converting a json value like {"index": "1"} to an int
               string tmpVal = stringValue.Replace("\"", string.Empty);
               prop.SetValue(target, tmpVal.ChangeType(type, Culture), null);
            }
            else if (type.IsEnum)
            {
               object converted = type.FindEnumValue(stringValue, Culture);
               prop.SetValue(target, converted, null);
            }
            else if (type == typeof (Uri))
            {
               Uri uri = new Uri(stringValue, UriKind.RelativeOrAbsolute);
               prop.SetValue(target, uri, null);
            }
            else if (type == typeof (string))
               prop.SetValue(target, stringValue, null);
            else if (type == typeof (DateTime) || type == typeof (DateTimeOffset))
            {
               DateTime dt = DateFormat.HasValue() ? DateTime.ParseExact(stringValue, DateFormat, Culture) : stringValue.ParseJsonDate(Culture);

               if (type == typeof (DateTime))
                  prop.SetValue(target, dt, null);
               else if (type == typeof (DateTimeOffset))
                  prop.SetValue(target, (DateTimeOffset) dt, null);
            }
            else if (type == typeof (Decimal))
            {
               decimal dec = Decimal.Parse(stringValue, Culture);
               prop.SetValue(target, dec, null);
            }
            else if (type == typeof (Guid))
            {
               Guid guid = string.IsNullOrEmpty(stringValue) ? Guid.Empty : new Guid(stringValue);
               prop.SetValue(target, guid, null);
            }
            else if (type == typeof (TimeSpan))
            {
               TimeSpan timeSpan = TimeSpan.Parse(stringValue);
               prop.SetValue(target, timeSpan, null);
            }
            else if (type.IsGenericType)
            {
               Type genericTypeDef = type.GetGenericTypeDefinition();
               if (genericTypeDef == typeof (List<>))
               {
                  IList list = BuildList(type, value);
                  prop.SetValue(target, list, null);
               }
               else if (genericTypeDef == typeof (Dictionary<,>))
               {
                  Type keyType = type.GetGenericArguments()[0];

                  // only supports Dict<string, T>()
                  if (keyType == typeof (string))
                  {
                     IDictionary dict = BuildDictionary(type, value);
                     prop.SetValue(target, dict, null);
                  }
               }
               else
               {
                  // nested property classes
                  object item = CreateAndMap(type, data[actualName]);
                  prop.SetValue(target, item, null);
               }
            }
            else
            {
               // nested property classes
               object item = CreateAndMap(type, data[actualName]);
               prop.SetValue(target, item, null);
            }
         }
      }

      private IDictionary BuildDictionary(Type type, object parent)
      {
         IDictionary dict = (IDictionary) Activator.CreateInstance(type);
         Type valueType = type.GetGenericArguments()[1];
         foreach (KeyValuePair<string, object> child in (IDictionary<string, object>) parent)
         {
            string key = child.Key;
            object item = CreateAndMap(valueType, child.Value);
            dict.Add(key, item);
         }

         return dict;
      }

      private IList BuildList(Type type, object parent)
      {
         IList list = (IList) Activator.CreateInstance(type);
         Type listType = type.GetInterfaces().First(x => x.GetGenericTypeDefinition() == typeof (IList<>));
         Type itemType = listType.GetGenericArguments()[0];

         if (parent is IList)
         {
            foreach (object element in (IList) parent)
            {
               if (itemType.IsPrimitive)
               {
                  string value = element.ToString();
                  list.Add(value.ChangeType(itemType, Culture));
               }
               else if (itemType == typeof (string))
               {
                  if (element == null)
                  {
                     list.Add(null);
                     continue;
                  }

                  list.Add(element.ToString());
               }
               else
               {
                  if (element == null)
                  {
                     list.Add(null);
                     continue;
                  }

                  object item = CreateAndMap(itemType, element);
                  list.Add(item);
               }
            }
         }
         else list.Add(CreateAndMap(itemType, parent));
         return list;
      }

      private object CreateAndMap(Type type, object element)
      {
         object instance = null;
         if (type.IsGenericType)
         {
            Type genericTypeDef = type.GetGenericTypeDefinition();
            if (genericTypeDef == typeof (Dictionary<,>))
               instance = BuildDictionary(type, element);
            else if (genericTypeDef == typeof (List<>))
               instance = BuildList(type, element);
            else if (type == typeof (string))
               instance = element;
            else
            {
               instance = Activator.CreateInstance(type);
               Map(instance, (IDictionary<string, object>) element);
            }
         }
         else if (type == typeof (string))
            instance = element.ToString();
         else
         {
            instance = Activator.CreateInstance(type);
            IDictionary<string, object> data = (IDictionary<string, object>) element;
            Map(instance, data);
         }
         return instance;
      }
   }

   public class JsonNameAttribute : Attribute
   {
      public string Name { get; set; }
   }
}